//! # Communication Protocol Definitions
//!
//! This module defines all message structures and protocols used for communication
//! between the node server and agents through RabbitMQ message queue.
//!
//! ## Message Types
//! - [`TaskMessage`]: Server to Agent command messages
//! - [`ResultMessage`]: Agent to Server result messages  
//! - [`AgentRegistration`]: Agent registration with server
//! - [`HeartbeatMessage`]: Agent heartbeat/status messages
//!
//! ## Protocol Features
//! - JSON-based message serialization
//! - Base64 encoding for binary data (scripts, files)
//! - UUID-based command tracking
//! - Group-based message routing
//! - Timestamp tracking for all messages

use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Server to Agent task/command message
///
/// This message is sent from the node server to agents through RabbitMQ.
/// It contains the command to execute and routing information.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TaskMessage {
    /// Unique identifier for tracking this command
    pub command_id: String,
    /// Type of command to execute (e.g., "get_hostname", "run_script")
    pub command_type: String,
    /// Command parameters as JSON value
    pub payload: serde_json::Value,
    /// Target group for routing (None means default group)
    pub target_group: Option<String>,
    /// Timestamp when command was created
    pub created_at: DateTime<Utc>,
}

impl TaskMessage {
    /// Create a hostname query command
    ///
    /// # Arguments
    /// * `target_group` - Optional group to target (None for default)
    pub fn new_get_hostname(target_group: Option<String>) -> Self {
        Self {
            command_id: Uuid::new_v4().to_string(),
            command_type: "get_hostname".to_string(),
            payload: serde_json::json!({}),
            target_group,
            created_at: Utc::now(),
        }
    }

    /// Create a script execution command
    ///
    /// # Arguments
    /// * `script_content` - Shell script content to execute
    /// * `timeout_sec` - Maximum execution time in seconds
    /// * `target_group` - Optional group to target (None for default)
    pub fn new_run_script(
        script_content: &str,
        timeout_sec: u32,
        target_group: Option<String>,
    ) -> Self {
        Self {
            command_id: Uuid::new_v4().to_string(),
            command_type: "run_script".to_string(),
            payload: serde_json::json!({
                "script_content": general_purpose::STANDARD.encode(script_content),
                "timeout_sec": timeout_sec
            }),
            target_group,
            created_at: Utc::now(),
        }
    }

    /// Create a file transfer command
    ///
    /// # Arguments
    /// * `file_path` - Destination path for the file on target agent
    /// * `file_content` - Binary content of the file
    /// * `target_group` - Optional group to target (None for default)
    pub fn new_receive_file(
        file_path: &str,
        file_content: &[u8],
        target_group: Option<String>,
    ) -> Self {
        Self {
            command_id: Uuid::new_v4().to_string(),
            command_type: "receive_file".to_string(),
            payload: serde_json::json!({
                "file_path": file_path,
                "file_content": general_purpose::STANDARD.encode(file_content)
            }),
            target_group,
            created_at: Utc::now(),
        }
    }
}

/// Agent to Server result message
///
/// This message is sent from agents back to the server with command execution results.
/// It includes success/failure status and the actual result data.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResultMessage {
    /// Command ID that this result corresponds to
    pub command_id: String,
    /// Agent ID that executed the command
    pub agent_id: String,
    /// Execution status: "success" or "error"
    pub status: String,
    /// Command execution result or error information
    pub result: serde_json::Value,
    /// Timestamp when command completed
    pub completed_at: DateTime<Utc>,
}

impl ResultMessage {
    /// Create a successful result message
    ///
    /// # Arguments
    /// * `command_id` - ID of the command that was executed
    /// * `agent_id` - ID of the agent that executed it
    /// * `result` - Execution result data
    pub fn success(command_id: String, agent_id: String, result: serde_json::Value) -> Self {
        Self {
            command_id,
            agent_id,
            status: "success".to_string(),
            result,
            completed_at: Utc::now(),
        }
    }

    /// Create an error result message
    ///
    /// # Arguments
    /// * `command_id` - ID of the command that failed
    /// * `agent_id` - ID of the agent where it failed
    /// * `error_msg` - Error description
    pub fn error(command_id: String, agent_id: String, error_msg: String) -> Self {
        Self {
            command_id,
            agent_id,
            status: "error".to_string(),
            result: serde_json::json!({
                "error": error_msg
            }),
            completed_at: Utc::now(),
        }
    }
}

/// Agent registration message
///
/// Sent by agents during startup to register with the server.
/// Contains agent metadata and authentication token.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AgentRegistration {
    /// Unique agent identifier
    pub agent_id: String,
    /// System hostname
    pub hostname: String,
    /// Groups this agent belongs to for task routing
    pub groups: Vec<String>,
    /// Authentication token for server verification
    pub token: String,
    /// Operating system information
    pub os_info: String,
    /// Registration timestamp
    pub registered_at: DateTime<Utc>,
}

impl AgentRegistration {
    /// Create a new agent registration message
    ///
    /// # Arguments
    /// * `agent_id` - Unique identifier for this agent
    /// * `hostname` - System hostname
    /// * `groups` - List of groups this agent belongs to
    /// * `token` - Authentication token
    /// * `os_info` - Operating system information string
    pub fn new(
        agent_id: String,
        hostname: String,
        groups: Vec<String>,
        token: String,
        os_info: String,
    ) -> Self {
        Self {
            agent_id,
            hostname,
            groups,
            token,
            os_info,
            registered_at: Utc::now(),
        }
    }
}

/// Agent heartbeat message
///
/// Periodically sent by agents to indicate they are still online and healthy.
/// Used by the server for health monitoring and connection management.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HeartbeatMessage {
    /// Agent identifier sending the heartbeat
    pub agent_id: String,
    /// Heartbeat timestamp
    pub timestamp: DateTime<Utc>,
    /// Agent status (typically "online")
    pub status: String,
}

impl HeartbeatMessage {
    /// Create a new heartbeat message
    ///
    /// # Arguments
    /// * `agent_id` - ID of the agent sending the heartbeat
    pub fn new(agent_id: String) -> Self {
        Self {
            agent_id,
            timestamp: Utc::now(),
            status: "online".to_string(),
        }
    }
}

/// RabbitMQ queue and exchange name constants
///
/// Defines the message routing topology used by the system.
pub mod queues {
    /// Exchange for routing task messages to agents
    pub const TASK_EXCHANGE: &str = "task_exchange";
    /// Exchange for routing result messages from agents
    pub const RESULT_EXCHANGE: &str = "result_exchange";
    /// Queue for agent registration messages
    pub const REGISTRATION_QUEUE: &str = "agent_registration";
    /// Queue for agent heartbeat messages
    pub const HEARTBEAT_QUEUE: &str = "agent_heartbeat";
    /// Queue for task result messages
    pub const RESULT_QUEUE: &str = "task_results";
}

/// Legacy command types (deprecated, kept for compatibility)
///
/// These are the original command types before the new TaskMessage protocol.
/// Will be removed in a future version.
#[derive(Serialize, Deserialize, Debug)]
pub enum CommandType {
    /// Simple ping command
    Ping,
    /// Get hostname command  
    GetHostname,
    /// Execute script command
    RunScript { script: String },
}

/// Legacy command structure (deprecated)
#[derive(Serialize, Deserialize)]
pub struct Command {
    /// Command identifier
    pub command_id: String,
    /// Command type and payload
    pub command_type: CommandType,
}

impl Command {
    /// Create a new ping command (deprecated)
    pub fn new_ping() -> Self {
        Self {
            command_id: Uuid::new_v4().to_string(),
            command_type: CommandType::Ping,
        }
    }

    /// Create a new hostname command (deprecated)
    pub fn new_get_hostname() -> Self {
        Self {
            command_id: Uuid::new_v4().to_string(),
            command_type: CommandType::GetHostname,
        }
    }
}

#[derive(Serialize, Deserialize)]
pub enum ResponseStatus {
    Success,
    Error(String),
}

#[derive(Serialize, Deserialize)]
pub struct Response {
    pub command_id: String,
    pub status: ResponseStatus,
    pub result: Option<String>,
}

#[cfg(test)]
mod tests {
    //! Unit tests for the protocol module

    use super::*;
    use chrono::Utc;
    use serde_json::json;
    use uuid::Uuid;

    mod task_message {
        use super::*;

        #[test]
        fn new_get_hostname_no_group() {
            let msg = TaskMessage::new_get_hostname(None);

            assert_eq!(msg.command_type, "get_hostname");
            assert_eq!(msg.payload, json!({}));
            assert_eq!(msg.target_group, None);
            assert!(!msg.command_id.is_empty());
            assert!(Uuid::parse_str(&msg.command_id).is_ok());
        }

        #[test]
        fn new_get_hostname_with_group() {
            let msg = TaskMessage::new_get_hostname(Some("web".to_string()));

            assert_eq!(msg.command_type, "get_hostname");
            assert_eq!(msg.payload, json!({}));
            assert_eq!(msg.target_group, Some("web".to_string()));
            assert!(!msg.command_id.is_empty());
        }

        #[test]
        fn new_run_script() {
            let script = "echo 'Hello World'";
            let timeout = 30;
            let msg = TaskMessage::new_run_script(script, timeout, Some("database".to_string()));

            assert_eq!(msg.command_type, "run_script");
            assert_eq!(msg.target_group, Some("database".to_string()));

            // Verify payload structure
            let payload = &msg.payload;
            assert!(payload["script_content"].is_string());
            assert_eq!(payload["timeout_sec"], 30);

            // Verify script is base64 encoded
            let encoded_script = payload["script_content"].as_str().unwrap();
            let decoded_script = base64::engine::general_purpose::STANDARD
                .decode(encoded_script)
                .unwrap();
            assert_eq!(String::from_utf8(decoded_script).unwrap(), script);
        }

        #[test]
        fn new_receive_file() {
            let file_path = "/tmp/test.txt";
            let file_content = b"Hello, World!";
            let msg = TaskMessage::new_receive_file(file_path, file_content, None);

            assert_eq!(msg.command_type, "receive_file");
            assert_eq!(msg.target_group, None);

            // Verify payload structure
            let payload = &msg.payload;
            assert_eq!(payload["file_path"], file_path);

            // Verify file content is base64 encoded
            let encoded_content = payload["file_content"].as_str().unwrap();
            let decoded_content = base64::engine::general_purpose::STANDARD
                .decode(encoded_content)
                .unwrap();
            assert_eq!(decoded_content, file_content);
        }
    }

    mod result_message {
        use super::*;

        #[test]
        fn success() {
            let command_id = "test-command-123".to_string();
            let agent_id = "test-agent-456".to_string();
            let result_data = json!({"hostname": "test-server", "uptime": 12345});

            let result_msg =
                ResultMessage::success(command_id.clone(), agent_id.clone(), result_data.clone());

            assert_eq!(result_msg.command_id, command_id);
            assert_eq!(result_msg.agent_id, agent_id);
            assert_eq!(result_msg.status, "success");
            assert_eq!(result_msg.result, result_data);
        }

        #[test]
        fn error() {
            let command_id = "test-command-123".to_string();
            let agent_id = "test-agent-456".to_string();
            let error_msg = "Command execution failed".to_string();

            let result_msg =
                ResultMessage::error(command_id.clone(), agent_id.clone(), error_msg.clone());

            assert_eq!(result_msg.command_id, command_id);
            assert_eq!(result_msg.agent_id, agent_id);
            assert_eq!(result_msg.status, "error");
            assert_eq!(result_msg.result["error"], error_msg);
        }
    }

    #[test]
    fn agent_registration_creation() {
        let agent_id = "agent-123".to_string();
        let hostname = "test-host".to_string();
        let groups = vec!["web".to_string(), "database".to_string()];
        let token = "secure_token_456".to_string();
        let os_info = "Linux Ubuntu 20.04".to_string();

        let registration = AgentRegistration::new(
            agent_id.clone(),
            hostname.clone(),
            groups.clone(),
            token.clone(),
            os_info.clone(),
        );

        assert_eq!(registration.agent_id, agent_id);
        assert_eq!(registration.hostname, hostname);
        assert_eq!(registration.groups, groups);
        assert_eq!(registration.token, token);
        assert_eq!(registration.os_info, os_info);
    }

    #[test]
    fn heartbeat_message_creation() {
        let agent_id = "test-agent-789".to_string();
        let heartbeat = HeartbeatMessage::new(agent_id.clone());

        assert_eq!(heartbeat.agent_id, agent_id);
        assert_eq!(heartbeat.status, "online");

        // Verify timestamp is recent (within last minute)
        let now = Utc::now();
        let diff = now.signed_duration_since(heartbeat.timestamp);
        assert!(diff.num_seconds() < 60);
    }

    #[test]
    fn queue_constants() {
        assert_eq!(queues::TASK_EXCHANGE, "task_exchange");
        assert_eq!(queues::RESULT_EXCHANGE, "result_exchange");
        assert_eq!(queues::REGISTRATION_QUEUE, "agent_registration");
        assert_eq!(queues::HEARTBEAT_QUEUE, "agent_heartbeat");
        assert_eq!(queues::RESULT_QUEUE, "task_results");
    }

    mod legacy_command {
        use super::*;

        #[test]
        fn command_ping() {
            let cmd = Command::new_ping();

            assert!(!cmd.command_id.is_empty());
            assert!(Uuid::parse_str(&cmd.command_id).is_ok());
            assert!(matches!(cmd.command_type, CommandType::Ping));
        }

        #[test]
        fn command_hostname() {
            let cmd = Command::new_get_hostname();

            assert!(!cmd.command_id.is_empty());
            assert!(matches!(cmd.command_type, CommandType::GetHostname));
        }
    }

    mod edge_case {
        use super::*;

        #[test]
        fn empty_script_content() {
            let msg = TaskMessage::new_run_script("", 10, None);
            let payload = &msg.payload;
            let encoded_script = payload["script_content"].as_str().unwrap();
            let decoded_script = base64::engine::general_purpose::STANDARD
                .decode(encoded_script)
                .unwrap();
            assert_eq!(String::from_utf8(decoded_script).unwrap(), "");
        }

        #[test]
        fn large_file_content() {
            // Test with 1MB of data
            let large_data = vec![0x42u8; 1024 * 1024];
            let msg = TaskMessage::new_receive_file("/tmp/large_file", &large_data, None);

            let payload = &msg.payload;
            let encoded_content = payload["file_content"].as_str().unwrap();
            let decoded_content = base64::engine::general_purpose::STANDARD
                .decode(encoded_content)
                .unwrap();
            assert_eq!(decoded_content, large_data);
        }

        #[test]
        fn unicode_script_content() {
            let script = "echo '你好世界'; echo 'Здравствуй мир'";
            let msg = TaskMessage::new_run_script(script, 30, None);

            let payload = &msg.payload;
            let encoded_script = payload["script_content"].as_str().unwrap();
            let decoded_script = base64::engine::general_purpose::STANDARD
                .decode(encoded_script)
                .unwrap();
            assert_eq!(String::from_utf8(decoded_script).unwrap(), script);
        }

        #[test]
        fn empty_groups_list() {
            let registration = AgentRegistration::new(
                "agent-id".to_string(),
                "hostname".to_string(),
                vec![], // Empty groups
                "token".to_string(),
                "os-info".to_string(),
            );

            assert!(registration.groups.is_empty());
        }

        #[test]
        fn special_characters_in_paths() {
            let special_path = "/tmp/file with spaces & symbols!@#$%^&*()";
            let msg = TaskMessage::new_receive_file(special_path, b"content", None);

            let payload = &msg.payload;
            assert_eq!(payload["file_path"], special_path);
        }
    }

    mod integration {
        use super::*;

        #[test]
        fn message_roundtrip() {
            // Create a task message
            let task = TaskMessage::new_run_script("echo 'test'", 30, Some("web".to_string()));

            // Serialize and deserialize
            let serialized = serde_json::to_string(&task).unwrap();
            let deserialized: TaskMessage = serde_json::from_str(&serialized).unwrap();

            // Create a result message for this task
            let result = ResultMessage::success(
                deserialized.command_id.clone(),
                "agent-123".to_string(),
                json!({"output": "test"}),
            );

            // Verify the command IDs match
            assert_eq!(task.command_id, result.command_id);
        }

        #[test]
        fn agent_workflow() {
            // Agent registers
            let registration = AgentRegistration::new(
                "agent-001".to_string(),
                "test-node".to_string(),
                vec!["web".to_string(), "api".to_string()],
                "secure_token".to_string(),
                "Linux Ubuntu 22.04".to_string(),
            );

            // Agent sends heartbeat
            let heartbeat = HeartbeatMessage::new(registration.agent_id.clone());

            // Server sends task
            let task = TaskMessage::new_get_hostname(Some("web".to_string()));

            // Agent sends result
            let result = ResultMessage::success(
                task.command_id.clone(),
                registration.agent_id.clone(),
                json!({"hostname": registration.hostname}),
            );

            // Verify agent IDs match throughout workflow
            assert_eq!(registration.agent_id, heartbeat.agent_id);
            assert_eq!(registration.agent_id, result.agent_id);
        }
    }
}
